ci(release): generate signed SBOM on each release - #41
Merged
Conversation
Adds supply-chain provenance to every GitHub Release so enterprise security teams (PCI-DSS / ISO 27001 / APRA CPS 234) can verify what shipped and prove it came from this repo's workflow. What's attached to each release now: - coda-sbom.cdx.json — CycloneDX SBOM (Python + npm deps via syft) - coda-sbom.cdx.json.cosign.bundle — cosign keyless signature bundle (cert + signature + Rekor inclusion proof) Signing uses GitHub OIDC — no long-lived keys. The signing identity is anchored to this workflow path and the release tag, and a public transparency-log entry is recorded in Rekor. Workflow changes: - Added `id-token: write` permission (required for OIDC keyless signing) - Added anchore/sbom-action step (SHA-pinned, format=cyclonedx-json) - Added sigstore/cosign-installer + sign-blob + in-workflow verify - Extended softprops/action-gh-release `files:` to attach both artefacts README changes: - New "Verifying release provenance" subsection with the cosign verify-blob command operators can run. Co-authored-by: Isaac
Keeps the README lean — release-provenance details now live in docs/SECURITY.md where security reviewers expect them. Also removes docs/plans/, which held historical design/implementation notes for features that have already shipped. Co-authored-by: Isaac
Collaborator
|
mpkrass7
self-requested a review
May 19, 2026 20:53
Updates the requirements on [pytest-playwright](https://github.com/microsoft/playwright-pytest) to permit the latest version. - [Release notes](https://github.com/microsoft/playwright-pytest/releases) - [Commits](microsoft/playwright-pytest@v0.5.0...v0.8.0) --- updated-dependencies: - dependency-name: pytest-playwright dependency-version: 0.8.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.1. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md) - [Commits](pallets/click@8.3.3...8.4.1) --- updated-dependencies: - dependency-name: click dependency-version: 8.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [idna](https://github.com/kjd/idna) from 3.16 to 3.17. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](kjd/idna@v3.16...v3.17) --- updated-dependencies: - dependency-name: idna dependency-version: '3.17' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [pydantic-core](https://github.com/pydantic/pydantic) from 2.46.4 to 2.47.0. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/commits) --- updated-dependencies: - dependency-name: pydantic-core dependency-version: 2.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@a309ff8...5fda3b9) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](softprops/action-gh-release@b430933...3d0d988) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@9c091bb...3d3c42e) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.1.0 to 9.0.0. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@0880764...c771a70) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
`pytest_collection_modifyitems` in tests/e2e/conftest.py receives the whole session's item list, not just this directory's. When the e2e prerequisites were missing (no recorded auth.json, or the databricks CLI not authed for the profile) it marked *every* collected test as skipped — so `uv run pytest tests/` reported "465 skipped" and the CI Tests workflow was passing vacuously. Filter to items that actually live under tests/e2e/. The unit suite now runs: 459 passed, 1 skipped.
…up_proxy The plan-doc cleanup removed 2026-03-11-litellm-empty-content-blocks-design.md, but app.py, setup_opencode.py and setup_proxy.py all point readers at it from code comments. Restore it so those pointers don't dangle.
dgokeeffe
added a commit
that referenced
this pull request
Aug 5, 2026
dgokeeffe
added a commit
that referenced
this pull request
Aug 5, 2026
…able Flask key (#40) * fix(cli-auth): atomic writes + observable failures on PAT rotation Hermes was returning 403 ("Invalid access token") on the first call after a PAT rotation, then succeeding on retry. Two reasons: 1. update_cli_tokens() rewrote each agent's config file with a bare open(path, "w"), creating a window where a concurrent Hermes invocation could read a half-written api_key line. Hermes is exposed to this because it re-reads ~/.hermes/config.yaml on every call; Claude/Codex/Gemini cache the token in env at process startup. 2. Every write path silently swallowed OSError, so an actual write failure (perms, locked file, ENOSPC) would leave the config stale forever with no log line — the user just saw 403s. Adds _atomic_write_text() helper (write to .tmp, os.replace) used by all five _update_* functions. Replaces silent except OSError: pass with logger.warning at WARNING level. FileNotFoundError still silenced via an explicit os.path.exists() guard so the rotator doesn't spam during the brief window between app start and setup script completion. Co-authored-by: Isaac * sec(enterprise): SECURITY.md + auth-leak fix + telemetry opt-out + stable Flask key Bundle of four enterprise-readiness improvements from an independent security review against NAB (APRA / CPS 234) and Coles (PCI-DSS / ISO 27001) procurement requirements. E-2: Add .github/SECURITY.md (P0 compliance blocker) ================================================================ Vendor security assessments (SIG / CAIQ) ask "do you have a documented vulnerability disclosure process?" — answer was no. Now we have: - Two private disclosure channels (GitHub Security Advisories + email) - Acknowledgement / triage / fix-plan / disclosure timeline - Severity-based patch SLA (Critical: 7d, High: 14d, Medium: 30d) - Scope boundaries (in/out of scope) - Supply-chain controls summary for procurement reviewers - Pointer to docs/enterprise.md § known limits for the trade-offs we've deliberately accepted E-4: Remove /api/app-state from auth-exempt list (P1) ================================================================ app.py:808 — /api/app-state was in the same exempt list as /health. Unauthenticated callers could fetch app_owner email + last_rotation_iso + owner_resolved_at, which is enough to fingerprint session timing and identify the workspace owner. The endpoint has no pre-auth use case in the UI (the polling endpoints the UI needs before PAT setup are /api/setup-status and /api/pat-status, both still exempt). Removed from the list. Tests: - test_app_state_denied_for_non_owner — non-owners get 403 - test_app_state_allowed_for_owner — owner still gets 200 E-9: Telemetry opt-out via CODA_TELEMETRY_DISABLED (P2) ================================================================ telemetry.py — log_telemetry() now short-circuits when CODA_TELEMETRY_DISABLED is truthy. Default behaviour unchanged (telemetry still on by default). Why this matters for procurement: regulated customers (banks, retailers with PCI-DSS) must inventory every outbound data flow from their workspace boundary. CoDA's User-Agent-header telemetry to Databricks servers was previously undisclosed and unsuppressible. Opt-out gives those customers a clean answer for their third-party-risk register. app.yaml gets a commented `CODA_TELEMETRY_DISABLED: "true"` example so operators see the knob. 5 new unit tests cover truthy/falsy parsing and that the background thread does NOT spawn when disabled. E-11: Stable Flask secret_key via FLASK_SECRET_KEY (P2) ================================================================ app.py:56 used to be `app.secret_key = os.urandom(24)` — regenerated on every worker restart, invalidating all Flask session cookies. With single-worker config today this is mostly cosmetic (sessions get torn down anyway when PTYs die with the worker), but it's a flagged finding in any key-management audit and would actively break sessions if we ever scaled to multi-worker. Now reads FLASK_SECRET_KEY from env (typically wired to a Databricks secret in app.yaml). Falls back to os.urandom for local dev with a WARNING log line so operators see when production is using an ephemeral key. app.yaml gets a commented example. Test results ================================================================ - 244/244 unit tests pass - 42/42 in the auth-enforcement + telemetry-opt-out + app-state suites - Default behaviour unchanged when no new env vars are set Out of scope (separate PRs / future work) ================================================================ - SBOM generation in the release workflow — PR #41 - Rate limiting on /api/configure-pat (E-3) — needs slowapi wire-up - Session-level command audit trail (E-8) — medium effort, structured logs - CSRF protection on /api/input (E-14) — medium effort - `requests = git+https://...` PyPI proxy bypass (E-6) — blocked by Databricks internal PyPI proxy availability Co-authored-by: Isaac * ci(release): generate signed SBOM on each release Adds supply-chain provenance to every GitHub Release so enterprise security teams (PCI-DSS / ISO 27001 / APRA CPS 234) can verify what shipped and prove it came from this repo's workflow. What's attached to each release now: - coda-sbom.cdx.json — CycloneDX SBOM (Python + npm deps via syft) - coda-sbom.cdx.json.cosign.bundle — cosign keyless signature bundle (cert + signature + Rekor inclusion proof) Signing uses GitHub OIDC — no long-lived keys. The signing identity is anchored to this workflow path and the release tag, and a public transparency-log entry is recorded in Rekor. Workflow changes: - Added `id-token: write` permission (required for OIDC keyless signing) - Added anchore/sbom-action step (SHA-pinned, format=cyclonedx-json) - Added sigstore/cosign-installer + sign-blob + in-workflow verify - Extended softprops/action-gh-release `files:` to attach both artefacts README changes: - New "Verifying release provenance" subsection with the cosign verify-blob command operators can run. Co-authored-by: Isaac * docs(legal): add Project Support / AS-IS disclaimer to README databrickslabs/dqx and other Labs projects ship a prominent "Project Support" block disclaiming SLAs and pointing users at the LICENSE for binding terms. CoDA has the LICENSE.md (with $1,000 aggregate liability cap + comprehensive warranty disclaimers) and NOTICE.md (full third-party attribution), but the README didn't surface either. Without this block, a user evaluating CoDA could plausibly assume it's a supported Databricks product because it lives under the databrickslabs/ org. The disclaimer closes that gap — and is placed immediately after the tagline (not buried at the bottom) because the tool exposes a workspace-credential-scoped shell and AI agents that act with full user authority. Text lifted from databrickslabs/dqx with one grammar tweak (singular "It is provided AS-IS" since this is one project, not multiple). Cross-references LICENSE.md and NOTICE.md so security-conscious readers can find the binding legal text in one hop. Co-authored-by: Isaac * fix(security): close unauth info-disclosure surface (setup-status, pat-status, app-state, /health) Closes the three P3/P4 info-disclosure findings from the 2026-05-17 pen test: 1. /api/setup-status was auth-exempt and returned the full setup_state dict including step error messages (stderr trimmed to 500 chars). If a setup script failed with leaky stderr — paths, version strings, env-var echoes — anyone hitting the URL could read it. 2. /api/pat-status was auth-exempt and returned workspace_host. Already known to anyone who reached the URL (they're auth'd to the same workspace), but principle-of-least-info says trim it. 3. /api/app-state was auth-exempt and returned app_owner email + last PAT rotation timestamp. PR #40 also removes this — including the same one-line fix here lets it ship sooner; PR #40 will rebase cleanly. 4. /health was auth-exempt and returned version, active_sessions, setup_status, session_timeout_seconds. Version exposure enables CVE-targeted exploit selection. Trimmed to {"status": "healthy"} — no internal or external caller needs the rich shape (the frontend doesn't poll /health; the Apps platform uses its own liveness mechanism upstream of the app). Removed setup-status, pat-status, and app-state from the before_request auth-exempt list. The frontend continues to poll all three because it loads from / (auth'd), so it already has SSO cookies. Test changes: - 5 new tests in TestInfoDisclosureEndpoints covering: setup-status denied/allowed, pat-status denied, app-state denied, /health minimal-response anti-leak (explicit assertions that version, setup_status, active_sessions are NOT in the response). - Updated test_session_linger.py: the 24h timeout assertion now reads SESSION_TIMEOUT_SECONDS from the module directly instead of via /health, since exposing the value to unauth callers was the leak we're closing. Stronger test, smaller attack surface. 236/236 unit tests pass. Co-authored-by: Isaac * docs: move SBOM verification to docs/SECURITY.md, drop stale plans Keeps the README lean — release-provenance details now live in docs/SECURITY.md where security reviewers expect them. Also removes docs/plans/, which held historical design/implementation notes for features that have already shipped. Co-authored-by: Isaac * chore(deps-dev): update pytest-playwright requirement Updates the requirements on [pytest-playwright](https://github.com/microsoft/playwright-pytest) to permit the latest version. - [Release notes](https://github.com/microsoft/playwright-pytest/releases) - [Commits](microsoft/playwright-pytest@v0.5.0...v0.8.0) --- updated-dependencies: - dependency-name: pytest-playwright dependency-version: 0.8.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump click from 8.3.3 to 8.4.1 Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.1. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md) - [Commits](pallets/click@8.3.3...8.4.1) --- updated-dependencies: - dependency-name: click dependency-version: 8.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump idna from 3.16 to 3.17 Bumps [idna](https://github.com/kjd/idna) from 3.16 to 3.17. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](kjd/idna@v3.16...v3.17) --- updated-dependencies: - dependency-name: idna dependency-version: '3.17' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump pydantic-core from 2.46.4 to 2.47.0 Bumps [pydantic-core](https://github.com/pydantic/pydantic) from 2.46.4 to 2.47.0. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/commits) --- updated-dependencies: - dependency-name: pydantic-core dependency-version: 2.47.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * docs: document Omnigent host integration Add a README section explaining the Omnigent host integration merged in #92: what it does, how to turn it on (with the app.yaml.lakemeter overlay), the two-credential model (SP-OAuth host tunnel + AI-Gateway harness LLM), the runtime control endpoints, and the ENABLE_SP_APIKEYHELPER pairing. Also add the OMNIGENTS_* / ENABLE_SP_APIKEYHELPER rows to the env-var reference table. The feature shipped in #92 with no user-facing docs (README had zero Omnigent mentions); this closes that gap. * docs: correct Omnigent identity and grant guidance * docs(auth): describe loopback SP token broker * chore(deps): bump actions/setup-python from 6.2.0 to 7.0.0 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@a309ff8...5fda3b9) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](softprops/action-gh-release@b430933...3d0d988) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@9c091bb...3d3c42e) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump astral-sh/setup-uv from 8.1.0 to 9.0.0 Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.1.0 to 9.0.0. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@0880764...c771a70) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix(tests): scope e2e prerequisite skip to the e2e directory `pytest_collection_modifyitems` in tests/e2e/conftest.py receives the whole session's item list, not just this directory's. When the e2e prerequisites were missing (no recorded auth.json, or the databricks CLI not authed for the profile) it marked *every* collected test as skipped — so `uv run pytest tests/` reported "465 skipped" and the CI Tests workflow was passing vacuously. Filter to items that actually live under tests/e2e/. The unit suite now runs: 459 passed, 1 skipped. * docs: keep litellm design doc referenced by app.py/setup_opencode/setup_proxy The plan-doc cleanup removed 2026-03-11-litellm-empty-content-blocks-design.md, but app.py, setup_opencode.py and setup_proxy.py all point readers at it from code comments. Restore it so those pointers don't dangle. * chore(config): declare every ENABLE_<CLI> toggle in each app.yaml overlay Apps overlays *replace* app.yaml rather than merging with it, and every setup script reads its toggle as `os.environ.get("ENABLE_<CLI>", "true")`. So a toggle omitted from an overlay is not a no-op — it silently re-enables that CLI's install on the deployed container. app.yaml and app.yaml.workshop listed all five; app.yaml.template and app.yaml.lakemeter listed only ENABLE_HERMES and ENABLE_PI, so deploys from those overlays would install Codex and Gemini even though neither has a compatible gateway endpoint — wasted boot time plus agents that fail on first request. Adds tests/test_app_yaml_overlays.py to hold the invariant: every tracked app.yaml* declares all five toggles, values stay quoted strings (unquoted `true` parses as a bool, which the scripts' .strip().lower() would choke on), and a new ENABLE_* appearing in setup_*.py fails the test until it's added to the overlays. This is the part of #30 that main hadn't already absorbed: the toggle mechanism itself landed separately, and main's default-on policy for Hermes/OpenCode is a deliberate later workshop decision. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: mpkrass7 <mpkrass@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every GitHub Release now publishes a signed CycloneDX SBOM, giving enterprise security reviewers a verifiable record of what shipped and proof that the bits came from this repo's workflow.
What's attached to each release going forward
coda-sbom.cdx.jsonanchore/sbom-action)coda-sbom.cdx.json.cosign.bundleHow signing works
cosign is invoked with
--yesand reads a short-lived OIDC token from GitHub Actions. There are no long-lived signing keys in this repo. The signing certificate's identity is bound to:https://github.com/databrickslabs/coding-agents-databricks-apps/.github/workflows/release.ymlrefs/tags/vX.Y.ZA public transparency-log entry is recorded in Rekor for every signature.
The workflow runs
cosign verify-blobagainst its own output before publishing — if signing or verification fails, the release is not created.Operator verification
Why enterprise reviewers care
Workflow changes
id-token: writepermission (required for cosign OIDC keyless signing)anchore/sbom-action@9f73021…(v0.20.5, SHA-pinned) — generatescyclonedx-jsonsigstore/cosign-installer@d7d6e07…(v3.5.0, SHA-pinned) +cosign sign-blobsoftprops/action-gh-releasefiles:to attach both artefacts to the releaseTrade-offs / what this PR is NOT
cosign sign <image-ref>alongside this.Test plan
python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))"passes locally)This pull request and its description were written by Isaac.